#include "limbo/errc.h"
#include "limbo/ip/state.hpp"
#include "limbo/udp/state.hpp"
#include "test-utils.h"

using namespace limbo;
using IpState = ip::State<void>;
using IpContext = typename IpState::Context;
using State = udp::State<IpState>;
using Context = typename State::Context;
using Packet = typename State::Packet;
static_assert(!details::has_lower_state_v<IpState>, "check0");
static_assert(details::has_lower_state_v<State>, "check1");
static_assert(std::is_same_v<typename Context::LowerContext, IpContext>,
              "check2");

uint8_t example_raw[]{0x14, 0xe9, 0x14, 0xe9, 0x00, 0x24, 0x00, 0x00, 0x00,
                      0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00,
                      0x00, 0x00, 0x04, 0x77, 0x70, 0x61, 0x64, 0x05, 0x6c,
                      0x6f, 0x63, 0x61, 0x6c, 0x00, 0x00, 0x01, 0x00, 0x01};
auto example_chunk = Chunk(example_raw, sizeof(example_raw));

uint8_t payload[] = {0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00,
                     0x00, 0x00, 0x04, 0x77, 0x70, 0x61, 0x64, 0x05, 0x6c, 0x6f,
                     0x63, 0x61, 0x6c, 0x00, 0x00, 0x01, 0x00, 0x01};
auto payload_chunk = Chunk(payload, sizeof(payload));

auto src = make_address("10.0.31.124");
auto dst = make_address("224.0.0.251");

TEST_CASE("send", "[udp]") {
  char buff[8 + sizeof(payload)];
  auto ip_ctx = IpContext{src, dst, 1, ip::Proto::udp, 0, 0, nullptr};
  auto ctx = Context{5353, 5353, &ip_ctx};
  auto state = State{};

  SECTION("no enough space") {
    auto zero_chunk = Chunk(buff, 0);
    auto result = Packet::send(state, ctx, zero_chunk, payload_chunk);
    REQUIRE(!result);
    CHECK(result.state() == &state);
    CHECK(result.error_code() == (uint32_t)Errc::no_enough_space);
  }

  auto buff_chunk = Chunk(buff, example_chunk.size());

  auto result = Packet::send(state, ctx, buff_chunk, payload_chunk);
  REQUIRE(result);
  auto res_buff = result.consumed();
  CHECK((void *)res_buff.data() == (void *)buff);
  CHECK(res_buff.size() == example_chunk.size());
  CHECK(res_buff == example_chunk);
}

TEST_CASE("recv", "[udp]") {
  auto ip_state = IpState();
  auto state = State{};
  auto result = state.recv(example_chunk, &ip_state);
  REQUIRE(result);
  CHECK(result.state() == &state);
  CHECK(result.consumed() == example_chunk);

  auto &packet = state.get_parsed();
  CHECK(packet.source_port == 5353);
  CHECK(packet.dest_port == 5353);
  CHECK(packet.length == example_chunk.size());
  CHECK(packet.checksum == 0);
  CHECK(packet.container == &ip_state.get_parsed());
  CHECK(packet.payload == payload_chunk);
}

TEST_CASE("recv/incomplete", "[udp]") {
  auto ip_state = IpState();
  auto state = State{};
  auto empty_chunk = Chunk(example_raw, 0);

  SECTION("less then header sz") {
    auto result = state.recv(Chunk(example_raw, 6), &ip_state);
    CHECK(result);
    CHECK(result.consumed() == empty_chunk);
    CHECK(result.demand() == 2);
  }

  SECTION("incomplete payload") {
    auto chunk = Chunk(example_raw, example_chunk.size() - 1);
    auto result = state.recv(chunk, &ip_state);
    CHECK(result);
    CHECK(result.consumed() == empty_chunk);
    CHECK(result.demand() == 1);
  }
}

TEST_CASE("checksums", "[udp]") {
  using State = udp::State<IpState, udp::Opts::validate_checksum>;

  uint8_t example_raw[]{0x00, 0x14, 0x00, 0x0a, 0x00,
                        0x0A, 0x35, 0xC5, 0x48, 0x69};
  auto example_chunk = Chunk(example_raw, sizeof(example_raw));
  auto ip_state = IpState();
  auto &ip = ip_state.get_parsed();
  ip.source = ip::IPv4Address::from_native(0xC0A8001F);
  ip.destination = ip::IPv4Address::from_native(0xC0A8001E);
  auto state = State();
  auto result = state.recv(example_chunk, &ip_state);
  REQUIRE(result);
  CHECK(result.state() == &state);
  CHECK(result.consumed() == example_chunk);
}

TEST_CASE("send/recv + checksum", "[udp]") {
  constexpr auto opts =
      udp::Opts::calculate_checksum | udp::Opts::validate_checksum;
  using IpState = ip::State<void>;
  using IpContext = typename IpState::Context;

  using State = udp::State<IpState, opts>;
  using Context = typename State::Context;
  using Packet = typename State::Packet;

  auto ip_state = IpState();
  char buff[8 + sizeof(payload)];
  auto ip_ctx = IpContext{src, dst, 1, ip::Proto::udp, 0, 0, nullptr};
  auto ctx = Context{5353, 5353, &ip_ctx};
  auto state = State{};
  auto buff_chunk = Chunk(buff, example_chunk.size());

  auto result = Packet::send(state, ctx, buff_chunk, payload_chunk);
  REQUIRE(result);
  auto res_buff = result.consumed();
  CHECK((void *)res_buff.data() == (void *)buff);
  CHECK(res_buff.size() == example_chunk.size());

  /* needed for correct pseudo header calculation */
  ip_state.init(0x6155);
  auto &ip_packet = ip_state.get_parsed();
  ip_packet.source = src;
  ip_packet.destination = dst;
  result = state.recv(buff_chunk, &ip_state);
  REQUIRE(result);
  CHECK(result.state() == &state);
}
